import { isError } from 'lodash'
import { BigNumber } from 'bignumber.js'

import { Wallet } from '@app/wallet'
import { BalanceManager } from '@app/balances'
import { logger } from '@pkg/logger'
import { sleep, nothrow } from '@pkg/promise'

import { Tasks } from '@app/storage/task'
import { Task, TaskType } from '@app/models/task'

import * as config from '@config/server.json'

class Worker {
    private wallet: Wallet
    private started = false
    private balances: BalanceManager

    constructor(wallet: Wallet, balances: BalanceManager) {
        this.wallet = wallet
        this.balances = balances
    }

    /**
     * 运行作业
     */
    async run() {
        if (this.started) {
            return
        }

        this.started = true
        while (this.started) {
            const task = await Tasks.first()
            if (task == null) {
                await sleep(5000)
                continue
            }

            try {
                if (task.type == TaskType.SendFee) {
                    await this.handleSendFee(task)
                } else if (task.type == TaskType.Collection) {
                    await this.handleCollection(task)
                } else {
                    await Tasks.setDone(task.id, 0)
                }
            } catch (error) {
                await Tasks.setDone(task.id, 0)
                logger.warn('[Worker] failed to exec task, %s', error)  
            }
        }
    }

    /**
     * 获取热钱包地址
     * @param symbol - 代币符号
     */
    private async getHotAddress(symbol: string) {
        if (symbol === 'TRX') {
            return this.wallet.tronWeb.address.fromPrivateKey(config.hotwallet)
        }

        const token = await this.wallet.getToken(symbol)
        if (!token) {
            return
        }
        if (!token.hotwallet) {
            return this.wallet.tronWeb.address.fromPrivateKey(config.hotwallet)
        }
        return this.wallet.tronWeb.address.fromPrivateKey(token.hotwallet)
    }

    /**
     * 获取资产负债表
     * @param symbol - 代币符号
     */
    private getBalanceSheet(symbol: string) {
        const balanceSheet = this.balances.getBalanceSheet(symbol)
        const trxBalanceSheet = symbol === 'TRX' ? balanceSheet : this.balances.getBalanceSheet('TRX')
        return {trxBalanceSheet, balanceSheet}
    }

    /**
     * 处理发送转账手续费
     * @param task - 任务信息
     */
    private async handleSendFee(task: Task) {
        const params = JSON.parse(task.params)
        const { trxBalanceSheet, balanceSheet } = this.getBalanceSheet(params.symbol)
        if (!trxBalanceSheet || !balanceSheet) {
            await Tasks.setDone(task.id as number, 0)
            return
        }

        let count = 0
        const fee = new BigNumber(config.fee)
        const minBalance = new BigNumber(params.minBalance)

        // 获取热钱包地址
        const hotAddress = await this.getHotAddress('TRX')
        if (!hotAddress) {
            await Tasks.setDone(task.id as number, count)
            return
        }

        // 获取发送方地址
        let senderAddress = hotAddress
        if (params.senderAddress) {
            senderAddress = params.senderAddress as string
        }

        // 遍历发送手续费
        await balanceSheet.forEach(async (address: string, balance: BigNumber) => {
            if (address === senderAddress) {
                return true
            }
            if (this.wallet.hotAddresses.has(address)) {
                return true
            }

            const trx = await trxBalanceSheet.getBalance(address)
            if (trx.comparedTo(fee) >= 0) {
                return true
            }

            if (balance.comparedTo(minBalance) < 0) {
                return true
            }

            const receipt = await nothrow(this.wallet.transferFrom(senderAddress, address, fee))
            if (isError(receipt)) {
                logger.warn('[Worker] failed to send fee, %s', receipt)
                return true
            }

            count++
            logger.info('[Worker] send fee(%s TRX) to %s, hash: %s', fee.toString(), address, receipt.txid)

            if (count > 0 && count % 50 == 0) {
                logger.info('[Worker] send fee transaction too many, count: %s, sleep...', count)
                await sleep(1000 * 60 * 10)
            }
            return true
        })
        await Tasks.setDone(task.id as number, count)
    }

    /**
     * 处理归集代币
     * @param task - 任务信息
     */
     private async handleCollection(task: Task) {
        const params = JSON.parse(task.params)
        const { trxBalanceSheet, balanceSheet } = this.getBalanceSheet(params.symbol)
        if (!trxBalanceSheet || !balanceSheet) {
            await Tasks.setDone(task.id as number, 0)
            return
        }

        let count = 0
        const minBalance = new BigNumber(params.minBalance)
        let minTrxBalance = new BigNumber(config.minTrxBalance)
        if (params.minTrxBalance) {
            minTrxBalance = new BigNumber(params.minTrxBalance as string)
        }

        // 获取热钱包地址
        const hotAddress = await this.getHotAddress(params.symbol)
        if (!hotAddress) {
            await Tasks.setDone(task.id as number, count)
            return
        }

        // 获取发送方地址
        let recipientAddress = hotAddress
        if (params.recipientAddress) {
            recipientAddress = params.recipientAddress as string
        }

        // 遍历归集代币到热钱包地址
        await balanceSheet.forEach(async (address: string, balance: BigNumber) => {
            if (address === recipientAddress) {
                return true
            }
            if (this.wallet.hotAddresses.has(address)) {
                return true
            }

            const trx = await trxBalanceSheet.getBalance(address)
            if (trx.comparedTo(minTrxBalance) < 0) {
                return true
            }

            if (balance.comparedTo(minBalance) < 0) {
                return true
            }

            let hash = ''
            if (params.symbol === 'TRX') {
                const amount = balance.minus(minTrxBalance)
                const receipt = await nothrow(this.wallet.transferFrom(address, recipientAddress, amount))
                if (isError(receipt)) {
                    logger.warn('[Worker] failed to fund collection, symbol: %s, from: %s, amount: %s, %s',
                        params.symbol, address, amount, receipt)
                    return true
                }
                hash = receipt.txid
            } else {
                const amount = new BigNumber(balance)
                const receipt = await nothrow(this.wallet.transferTokenFrom(params.symbol, address, recipientAddress, amount))
                if (isError(receipt)) {
                    logger.warn('[Worker] failed to fund collection, symbol: %s, from: %s, amount: %s, %s',
                        params.symbol, address, amount, receipt)
                    return true
                }
                hash = receipt.txid
            }
            count++
            logger.info('[Worker] fund collection(%s %s) from %s to %s, hash: %s',
                balance, params.symbol, address, recipientAddress, hash)

            if (count > 0 && count % 50 == 0) {
                logger.info('[Worker] fund collection transaction too many, count: %s, sleep...', count)
                await sleep(1000 * 60 * 10)
            }
            return true
        })
        await Tasks.setDone(task.id as number, count)
    }
}

export { Worker }
